//
//  main.cpp
//  mac_cddvdrives_enum
//
//  Created by Constantine Mars on 7/1/13.
//  Copyright (c) 2013 Constantine Mars. All rights reserved.
//

#include <iostream>
#include <algorithm>
#include <sstream>
#include <boost/filesystem.hpp>
#include <boost/iostreams/device/file_descriptor.hpp>
#include <boost/iostreams/stream.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/algorithm/string.hpp>
#include <boost/algorithm/string/regex.hpp>
#include <CoreFoundation/CoreFoundation.h>

namespace
{
    typedef struct BUFFER {
        uint32_t BufferLength;
        uint8_t *Buffer;
    } BUFFER;
    
    namespace io = boost::iostreams;
    
#define FLAG_PRESENT 0x0001
#define FLAG_ABSENT 0x0001
    
    struct cdromdev_record_t
    {
        std::string volumeName;
        char vol_name[1];
        unsigned char disk_num; // Disk number. Starting from 0. 0 == 'A:', 1 == 'B:', ... - what if there is more than 9 disks in system? if there is "disk12s55" ?
        unsigned char flags; // FLAG_PRESENT = 0x0001
        std::string uuid; // added to exclude repeating of the same data in output vector
        void print() const;
    };
    
    //  TODO remove this debug-time output
    void cdromdev_record_t::print() const
    {
        std::cout << volumeName
        << " " << std::dec << (size_t)disk_num
        << " 0x" << std::hex << (size_t)flags
        << std::endl;
    }
    
    //  TODO remove this debug-time output
    void printDriveInfo(const cdromdev_record_t& driveInfo)
    {
        driveInfo.print();
    }
    
    typedef std::vector<std::string> StrVector;
    typedef std::vector<cdromdev_record_t> OpticalDriveInfoVector;
    
    std::string popen2string(const char* pipe, const char delim)
    {
        FILE* file = popen(pipe, "r");
        if (!file)
            throw std::runtime_error("failed to popen.");
        boost::shared_ptr<FILE> pipe_guard(file, pclose);
        
        io::stream<io::file_descriptor_source> in(io::file_descriptor_source(fileno(file), io::never_close_handle));
        
        std::string commandOutput;
        std::getline(in, commandOutput, delim);
        
        return commandOutput;
    }
    
    // convert disk12s55 to 1255
    size_t getNumericId(std::string keyword)
    {
        std::string numString;
        for (std::string::iterator it = keyword.begin(); it != keyword.end(); ++it)
        {
            if ( !isdigit(*it) )
            {
                continue;
            }
            
            numString += *it;
        }
        
        std::stringstream ss(numString);
        size_t num;
        ss >> num;
        return num;
    }
    
    bool noFileSystem(StrVector& driveInfo)
    {
        bool result = false;
        
        for(StrVector::iterator it = driveInfo.begin(); it != driveInfo.end(); ++it)
        {
            if( (*it).find("File System:") != std::string::npos)
            {
                StrVector keywords;
                boost::algorithm::split(keywords, (*it), boost::algorithm::is_any_of(" \t\n"));
                size_t length = keywords.size();
                if (length > 1)
                {
                    if (boost::iequals(keywords[length - 1], "None"))
                    {
                        result = true;
                        break;
                    }
                }
            }
        }
        
        return result;
    }
    
    void splitInfoString(StrVector& keywords, const std::string& infoString)
    {
        boost::algorithm::split_regex(keywords, infoString, boost::regex( "[ ]{2,}" ));
    }
    
    void getDriveInfo(std::string infoString, cdromdev_record_t* pOdInfo)
    {
        if ( !pOdInfo )
            return;
        
        if(infoString.find("Volume Name:") != std::string::npos)
        {
            StrVector keywords;
            splitInfoString(keywords, infoString);
            
            size_t length = keywords.size();
            if (length > 1)
            {
                pOdInfo->volumeName = keywords[length - 1];
            }
        }
        else if(infoString.find("Device Node:") != std::string::npos)
        {
            StrVector keywords;
            splitInfoString(keywords, infoString);
            size_t length = keywords.size();
            if (length > 1)
            {
                // what if there is more than 9 disks in system? if there is "disk12s55" - we try to "concatenate" id from major and minor numbers both... but is it right?
                // "Part of Whole:" always returns major disk number, for example "disk12"
                // "Device Node:" returns this partition
                pOdInfo->disk_num = getNumericId(keywords[length - 1]); // we return size_t and use implicit cast to unsigned char, for example, 55 => '7' - is it right?
            }
        }
        else if(infoString.find("Mounted:") != std::string::npos)
        {
            StrVector keywords;
            splitInfoString(keywords, infoString);
            size_t length = keywords.size();
            if (length > 1)
            {
                std::string mounted = keywords[length - 1];
                pOdInfo->flags = ( (boost::iequals(mounted, "YES") || boost::iequals(mounted, "1")) ? FLAG_PRESENT : FLAG_ABSENT);
            }
        }
        else if(infoString.find("Volume UUID:") != std::string::npos)
        {
            StrVector keywords;
            splitInfoString(keywords, infoString);
            size_t length = keywords.size();
            if (length > 1)
            {
                pOdInfo->uuid = keywords[length - 1];
            }
        }
    }
    
    void getDriveInfoWithPlist(const std::string& drive, cdromdev_record_t& odInfo)
    {
        std::string  command = "diskutil info -plist ";
        command.append(drive);
        
        std::string plist = popen2string(command.c_str(), 0xFF);
        if (!plist.length())
            return;
        
        CFDataRef propertyListDataRef = CFDataCreateWithBytesNoCopy( kCFAllocatorDefault,
                                                                    (const UInt8*)plist.data(), // what is the proper way to cast?
                                                                    plist.size(),
                                                                    kCFAllocatorNull );
        if( !propertyListDataRef )
            return;
        
        CFPropertyListRef propertyListRef = CFPropertyListCreateWithData(kCFAllocatorDefault,
                                                                         propertyListDataRef,
                                                                         kCFPropertyListImmutable,
                                                                         NULL,
                                                                         NULL);
        if( !propertyListRef )
        {
            CFRelease( propertyListDataRef );
            return;
        }
        
        //
        // TO DO - get OpticalDeviceType and if not empty get VolumeName, if not empty push it to names
        if( CFGetTypeID( propertyListRef ) == CFDictionaryGetTypeID() ){
            
            CFStringRef volumeName = (CFStringRef)CFDictionaryGetValue( (CFDictionaryRef)propertyListRef, CFSTR("VolumeName") ); // human readable is "Volume Name"
            if( volumeName && CFGetTypeID(volumeName) == CFStringGetTypeID() && 0x0 != CFStringGetLength(volumeName) )
            {
                
                CFIndex length = CFStringGetLength(volumeName) + 1;
                char* cstr = (char *)malloc( length + 1 );
                bool conversionResult = CFStringGetCString(volumeName, cstr, length, kCFStringEncodingUTF8 );
                if (conversionResult)
                {
                    odInfo.volumeName = cstr;
                }
                free( cstr );
            }
            
            CFStringRef deviceNode = (CFStringRef)CFDictionaryGetValue( (CFDictionaryRef)propertyListRef, CFSTR("DeviceNode") ); // human readable is "Device Node"
            if( deviceNode && CFGetTypeID(deviceNode) == CFStringGetTypeID() && 0x0 != CFStringGetLength(deviceNode) )
            {
                
                CFIndex length = CFStringGetLength(deviceNode) + 1;
                char* cstr = (char *)malloc( length + 1 );
                bool conversionResult = CFStringGetCString(deviceNode, cstr, length, kCFStringEncodingUTF8 );
                if (conversionResult)
                {
                    odInfo.disk_num = getNumericId(cstr);
                }
                free( cstr );
            }
            
            odInfo.flags = FLAG_ABSENT;
            CFStringRef mountPoint = (CFStringRef)CFDictionaryGetValue( (CFDictionaryRef)propertyListRef, CFSTR("MountPoint") ); // human readable is "Mount Point", but "Mounted" is enough
            if( mountPoint && CFGetTypeID(mountPoint) == CFStringGetTypeID() && 0x0 != CFStringGetLength(mountPoint) )
            {
                CFIndex length = CFStringGetLength(mountPoint);
                // media is mounted if MountPoint is not empty
                if (length > 0)
                {
                    odInfo.flags = FLAG_PRESENT;
                }
            }
        }
        
        CFRelease( propertyListRef );
        CFRelease( propertyListDataRef );
    }
    
    bool CheckUuid(cdromdev_record_t odInfo, const std::string* uuid)
    {
        if ( !uuid )
            return false;
        
        return boost::iequals(odInfo.uuid, *uuid);
    }
    
    void AddIfOptical(std::string drive, OpticalDriveInfoVector* opticalDrives)
    {
        if ( !opticalDrives )
            return;
        
        std::string  command = "diskutil info ";
        command.append(drive);
        
        std::string driveInfoString = popen2string(command.c_str(), 0xFF);
        if (!driveInfoString.length())
            return;
        
        StrVector driveInfo;
        boost::algorithm::split(driveInfo, driveInfoString, boost::algorithm::is_any_of("\n"));
        
        for (StrVector::iterator it = driveInfo.begin(); it != driveInfo.end(); ++it)
        {
            bool isOptical = ((*it).find("Optical") != std::string::npos);
            if (isOptical)
            {
                //                //TODO remove this debug output
                //                std::cout << "----OPTICAL DRIVE----------\n";
                //                std::copy(driveInfo.begin(), driveInfo.end(), std::ostream_iterator<std::string>(std::cout, "\n"));
                //                std::cout << "---------------------------\n";
                
                
                if (noFileSystem(driveInfo)) // disk without filesystem can't be mounted and can't have volume name (label)
                    continue;
                
                cdromdev_record_t odInfo;
                std::for_each(driveInfo.begin(), driveInfo.end(), std::bind2nd(std::ptr_fun(&getDriveInfo), &odInfo)); // "clear" stl/boost parsing of human-readable output
                //            getDriveInfoWithPlist(drive, odInfo); // plist output parsing according to original example
                
                // check if such uuid already added
                if (std::find_if(opticalDrives->begin(), opticalDrives->end(), std::bind2nd(std::ptr_fun(&CheckUuid), &odInfo.uuid)) != opticalDrives->end())
                {
                    continue;
                }
                
                opticalDrives->push_back(odInfo);
                //                //TODO remove this debug output
                //                odInfo.print();
            }
        }
    };
    
#define INIT_BUFFER(b)       (void)((b)->Buffer = 0, (b)->BufferLength = 0)
    
}



int GrabCDROMDevices(unsigned int dwFlags, BUFFER *pDevicesList)
{
    // TODO:
    // 1. implement header creation
    // 2. implement serialization to BUFFER
    // 3. move popen2string somewhere to Common
    
    std::clog << "GrabCDROMDevices is called\n";
    
    std::string drivesString = popen2string("ls /dev/ | grep ^disk[0-9]", 0xFF);
    if (!drivesString.length())
        return ENOENT;
    
    StrVector drives;
    boost::algorithm::split(drives, drivesString, boost::algorithm::is_any_of(" \n"));
    
    OpticalDriveInfoVector opticalDrives;
    std::for_each(drives.begin(), drives.end(), std::bind2nd(std::ptr_fun(&AddIfOptical), &opticalDrives));
    
    INIT_BUFFER(pDevicesList);
   
//    RPCCALL_STATUS_PARAM (pDevicesList->Buffer = (uint8_t *)rpc_sm_allocate, 4);
    memset(pDevicesList->Buffer, 0, pDevicesList->BufferLength);
    
//    RPCCALL_STATUS_PARAM (pDevicesList->Buffer = (uint8_t *)rpc_sm_allocate, blob.size());
    pDevicesList->BufferLength = static_cast<uint32_t>(opticalDrives.size());
    
    if (!opticalDrives.empty())
    {
        memcpy(pDevicesList->Buffer, &opticalDrives[0], opticalDrives.size());
    }
    
    //  //TODO remove this debug-time output
    //    std::for_each(opticalDrives.begin(), opticalDrives.end(), printDriveInfo);
    
    return 0;
}

int main(int argc, const char * argv[])
{
    unsigned int dwFlags = 0x00;
    BUFFER devicesList;
    
    GrabCDROMDevices(dwFlags, &devicesList);
    
    
    return 0;
}

